diff --git a/examples/reference/panes/Markdown.ipynb b/examples/reference/panes/Markdown.ipynb
index ef4f0055e1..1f1a311215 100644
--- a/examples/reference/panes/Markdown.ipynb
+++ b/examples/reference/panes/Markdown.ipynb
@@ -23,10 +23,11 @@
     "\n",
     "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
     "\n",
-    "* **`dedent`** (bool): Whether to dedent common whitespace across all lines.\n",
+    "* **`dedent`** (bool, `default=True`): Whether to dedent common whitespace across all lines.\n",
     "* **`disable_math`** (boolean, `default=False`): Whether to disable MathJax math rendering for strings escaped with `$$` delimiters.\n",
     "* **`enable_streaming`** (boolean, `default=False`): Whether to enable streaming of text snippets. This will diff the `object` when it is updated and only send the trailing chunk that was added.\n",
     "* **`extensions`** (list): A list of [Python-Markdown extensions](https://python-markdown.github.io/extensions/) to use (does not apply for 'markdown-it' and 'myst' renderers).\n",
+    "* **`hard_line_break`** (bool, `default=False`): Whether simple new lines are rendered as hard line breaks. False by default to conform with the original Markdown spec. Not supported by the `'myst'` renderer.\n",
     "* **`object`** (str or object): A string containing Markdown, or an object with a ``_repr_markdown_`` method.\n",
     "* **`plugins`** (function): A list of additional markdown-it-py plugins to apply.\n",
     "* **`renderer`** (literal: `'markdown-it'`, `'markdown'`, `'myst'`): Markdown renderer implementation.\n",
@@ -349,6 +350,59 @@
    "source": [
     "pn.pane.Markdown(r\"$$\\frac{1}{n}$$\")"
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## New Lines\n",
+    "\n",
+    "Markdown renderers typically do not interpret new lines as hard line breaks. According to the Markdown standard, a hard line break requires the line to end with either *two spaces* or a *backslash*. Otherwise, it is treated as a soft line break. By default, Panel adheres to this standard, respecting the behavior where simple new lines are not displayed as hard line breaks."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pn.pane.Markdown(\"\"\"\n",
+    "Markdown displayed\n",
+    "on two lines.\n",
+    "\"\"\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Rendering simple new lines as hard line breaks can be enabled for all renderers but `'myst'` by setting `hard_line_break` to `True`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pn.pane.Markdown(\"\"\"\n",
+    "Markdown displayed\n",
+    "on one line.\n",
+    "\n",
+    "Markdown displayed on multiple lines  \n",
+    "as each line ends with two spaces,  \n",
+    "despite hard_line_break=True\n",
+    "\"\"\", hard_line_break=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    ":::{hint}\n",
+    "`hard_line_break=True` is appropriate for displaying Markdown text generated by LLMs.\n",
+    ":::\n"
+   ]
   }
  ],
  "metadata": {
diff --git a/panel/chat/message.py b/panel/chat/message.py
index 395a70c81a..f26accef53 100644
--- a/panel/chat/message.py
+++ b/panel/chat/message.py
@@ -515,6 +515,8 @@ def _create_panel(self, value, old=None):
                 self._set_params(old, enable_streaming=True, object=value)
                 return old
             object_panel = _panel(value)
+            if isinstance(object_panel, Markdown) and not object_panel.hard_line_break:
+                object_panel.hard_line_break = True
 
         self._set_params(object_panel)
         if type(old) is type(object_panel) and self._internal:
diff --git a/panel/pane/markup.py b/panel/pane/markup.py
index b14b930c2d..18a3bd0ef2 100644
--- a/panel/pane/markup.py
+++ b/panel/pane/markup.py
@@ -357,6 +357,11 @@ class Markdown(HTMLBasePane):
         Markdown extension to apply when transforming markup.
         Does not apply if renderer is set to 'markdown-it' or 'myst'.""")
 
+    hard_line_break = param.Boolean(default=False, doc="""
+        Whether simple new lines are rendered as hard line breaks. False by
+        default to conform with the original Markdown spec. Not supported by
+        the 'myst' renderer.""")
+
     plugins = param.List(default=[], nested_refs=True, doc="""
         Additional markdown-it-py plugins to use.""")
 
@@ -371,6 +376,7 @@ class Markdown(HTMLBasePane):
     priority: ClassVar[float | bool | None] = None
 
     _rename: ClassVar[Mapping[str, str | None]] = {
+        'hard_line_break': None,
         'dedent': None, 'disable_math': None, 'extensions': None,
         'plugins': None, 'renderer': None, 'renderer_options': None
     }
@@ -398,7 +404,7 @@ def applies(cls, obj: Any) -> float | bool | None:
 
     @classmethod
     @functools.cache
-    def _get_parser(cls, renderer, plugins, **renderer_options):
+    def _get_parser(cls, renderer, plugins, hard_line_break, **renderer_options):
         if renderer == 'markdown':
             return None
         from markdown_it import MarkdownIt
@@ -416,7 +422,7 @@ def hilite(token, langname, attrs):
                 return token
 
         if renderer == 'markdown-it':
-            if "breaks" not in renderer_options:
+            if hard_line_break and "breaks" not in renderer_options:
                 renderer_options["breaks"] = True
 
             parser = MarkdownIt(
@@ -458,15 +464,16 @@ def _transform_object(self, obj: Any) -> dict[str, Any]:
             obj = textwrap.dedent(obj)
 
         if self.renderer == 'markdown':
+            extensions = self.extensions + ['nl2br'] if self.hard_line_break else self.extensions
             html = markdown.markdown(
                 obj,
-                extensions=self.extensions,
+                extensions=extensions,
                 output_format='xhtml',
                 **self.renderer_options
             )
         else:
             parser = self._get_parser(
-                self.renderer, tuple(self.plugins), **self.renderer_options
+                self.renderer, tuple(self.plugins), self.hard_line_break, **self.renderer_options
             )
             try:
                 html = parser.render(obj)
diff --git a/panel/tests/pane/test_markup.py b/panel/tests/pane/test_markup.py
index 0330ab8460..0e52cccb7c 100644
--- a/panel/tests/pane/test_markup.py
+++ b/panel/tests/pane/test_markup.py
@@ -1,4 +1,5 @@
 import base64
+import html
 import json
 import sys
 
@@ -78,33 +79,73 @@ def test_markdown_pane_dedent(document, comm):
     pane.dedent = False
     assert model.text.startswith('<pre><code>ABC')
 
-def test_markdown_pane_newline(document, comm):
-    # Newlines should be separated by a br
-    pane = Markdown(
-        "Hello\nWorld\nI'm here!",
-        renderer="markdown-it",
-    )
+@pytest.mark.parametrize('renderer', ('markdown-it', 'markdown'))
+def test_markdown_pane_hard_line_break_default(document, comm, renderer):
+    assert Markdown.hard_line_break is False
+    txt = "Hello\nWorld\nI am here"
+    pane = Markdown(txt, renderer=renderer)
     model = pane.get_root(document, comm=comm)
     assert pane._models[model.ref['id']][0] is model
-    # <p>Hello<br>World<br>I'm here!</p>
-    assert model.text == "&lt;p&gt;Hello&lt;br /&gt;\nWorld&lt;br /&gt;\nI&#x27;m here!&lt;/p&gt;\n"
+    # No <br />, single <p>
+    assert html.unescape(model.text).rstrip() == f"<p>{txt}</p>"
 
-    # Two newlines should be separated by a div
-    pane = Markdown("Hello\n\nWorld")
+@pytest.mark.parametrize('renderer', ('markdown-it', 'markdown'))
+def test_markdown_pane_hard_line_break_enabled(document, comm, renderer):
+    assert Markdown.hard_line_break is False
+    pane = Markdown("Hello\nWorld\nI am here", renderer=renderer, hard_line_break=True)
+    model = pane.get_root(document, comm=comm)
+    assert pane._models[model.ref['id']][0] is model
+    # Two <br />, single <p>
+    assert html.unescape(model.text).rstrip() == "<p>Hello<br />\nWorld<br />\nI am here</p>"
+
+@pytest.mark.parametrize('hard_line_break', (False, True))
+def test_markdown_pane_hard_line_break_myst(document, comm, hard_line_break):
+    pytest.importorskip("myst_parser")
+    # hard_line_break not supported
+    assert Markdown.hard_line_break is False
+    txt = "Hello\nWorld\nI am here"
+    pane = Markdown(txt, renderer='myst', hard_line_break=hard_line_break)
+    model = pane.get_root(document, comm=comm)
+    assert pane._models[model.ref['id']][0] is model
+    # No <br />, single <p>
+    assert html.unescape(model.text).rstrip() == f"<p>{txt}</p>"
+
+@pytest.mark.parametrize('renderer', ('markdown-it', 'markdown', 'myst'))
+@pytest.mark.parametrize('hard_line_break', (False, True))
+def test_markdown_pane_hard_line_break_default_two_spaces(document, comm, renderer, hard_line_break):
+    if renderer == 'myst':
+        pytest.importorskip("myst_parser")
+    # Same output, whether hard_line_break is True or False
+    assert Markdown.hard_line_break is False
+    # Note the two empty spaces at the end of each line.
+    pane = Markdown("Hello  \nWorld  \nI am here", renderer=renderer, hard_line_break=hard_line_break)
+    model = pane.get_root(document, comm=comm)
+    assert pane._models[model.ref['id']][0] is model
+    # Two <br />, single <p>
+    assert html.unescape(model.text).rstrip() == "<p>Hello<br />\nWorld<br />\nI am here</p>"
+
+@pytest.mark.parametrize('renderer', ('markdown-it', 'markdown', 'myst'))
+def test_markdown_pane_two_new_lines(document, comm, renderer):
+    if renderer == 'myst':
+        pytest.importorskip("myst_parser")
+    assert Markdown.hard_line_break is False
+    pane = Markdown("Hello\n\nWorld", renderer=renderer)
     model = pane.get_root(document, comm=comm)
     assert pane._models[model.ref['id']][0] is model
-    # <p>Hello</p><p>World</p>
-    assert model.text == "&lt;p&gt;Hello&lt;/p&gt;\n&lt;p&gt;World&lt;/p&gt;\n"
+    # Two <p> elements
+    assert html.unescape(model.text).rstrip() == "<p>Hello</p>\n<p>World</p>"
 
-    # Disable newlines
+def test_markdown_pane_markdown_it_render_options_breaks(document, comm):
+    assert Markdown.hard_line_break is False
     pane = Markdown(
-        "Hello\nWorld\nI'm here!",
+        "Hello\nWorld\nI am here",
         renderer="markdown-it",
-        renderer_options={"breaks": False},
+        renderer_options={"breaks": True},
     )
     model = pane.get_root(document, comm=comm)
     assert pane._models[model.ref['id']][0] is model
-    assert model.text == "&lt;p&gt;Hello\nWorld\nI&#x27;m here!&lt;/p&gt;\n"
+    # Two <br />, single <p>
+    assert html.unescape(model.text).rstrip() == "<p>Hello<br />\nWorld<br />\nI am here</p>"
 
 def test_markdown_pane_markdown_it_renderer(document, comm):
     pane = Markdown("""
diff --git a/panel/tests/ui/pane/test_markup.py b/panel/tests/ui/pane/test_markup.py
index 71399b79d0..e47633d39f 100644
--- a/panel/tests/ui/pane/test_markup.py
+++ b/panel/tests/ui/pane/test_markup.py
@@ -121,7 +121,7 @@ def test_anchor_scroll(page):
     md = ''
     for tag in ['tag1', 'tag2', 'tag3']:
         md += f'# {tag}\n\n'
-        md += f'{tag} content\n' * 50
+        md += f'{tag} content  \n' * 50
 
     content = Markdown(md)
     link = Markdown('<a id="link1" href="#tag1">Link1</a><a id="link3" href="#tag3">Link</a>')
@@ -145,7 +145,7 @@ def test_anchor_scroll_on_init(page):
     md = ''
     for tag in ['tag1', 'tag2', 'tag3']:
         md += f'# {tag}\n\n'
-        md += f'{tag} content\n' * 50
+        md += f'{tag} content  \n' * 50
 
     content = Markdown(md)
 
diff --git a/panel/widgets/base.py b/panel/widgets/base.py
index 78fcebc790..22a4d281ed 100644
--- a/panel/widgets/base.py
+++ b/panel/widgets/base.py
@@ -134,7 +134,7 @@ def _process_param_change(self, params: dict[str, Any]) -> dict[str, Any]:
             renderer_options = params.pop("renderer_options", {})
             if isinstance(description, str):
                 from ..pane.markup import Markdown
-                parser = Markdown._get_parser('markdown-it', (), **renderer_options)
+                parser = Markdown._get_parser('markdown-it', (), Markdown.hard_line_break, **renderer_options)
                 html = parser.render(description)
                 params['description'] = Tooltip(
                     content=HTML(html), position='right',