diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index fefe9f3031..46d65bdafd 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -1027,8 +1027,6 @@
       // $(response.data) as new HTML rather than a CSS selector. Also, if
       // response.data contains top-level text nodes, they get lost with either
       // $(response.data) or $('<div></div>').replaceWith(response.data).
-      var $new_content_wrapped = $('<div></div>').html(response.data);
-      var $new_content = $new_content_wrapped.contents();
 
       // For legacy reasons, the effects processing code assumes that
       // $new_content consists of a single top-level element. Also, it has not
@@ -1041,10 +1039,44 @@
       // of a single top-level element, and only use the container <div> created
       // above when it doesn't. For more information, please see
       // https://www.drupal.org/node/736066.
-      if ($new_content.length !== 1 || $new_content.get(0).nodeType !== 1) {
-        $new_content = $new_content_wrapped;
+
+      var $responseDataWrapped = $('<div></div>').html(response.data);
+      var $new_content = $('<div></div>');
+      var elementsReturned = $responseDataWrapped.contents().length;
+
+      var createWrapper = function () {
+        return $('<div></div>');
+      };
+
+      var intermediateWrapper = null;
+      $responseDataWrapped.contents().each(function (index, value) {
+        if (intermediateWrapper && value.nodeType !== 1) {
+          intermediateWrapper.append(value);
+        }
+        if (!intermediateWrapper && value.nodeType !== 1) {
+          if ($.trim(value.nodeValue).length > 0) {
+            intermediateWrapper = createWrapper();
+            intermediateWrapper.append(value);
+          }
+        }
+        if (!intermediateWrapper && value.nodeType === 1) {
+          $new_content.append(value);
+          intermediateWrapper = null;
+        }
+        if (intermediateWrapper && value.nodeType === 1) {
+          $new_content.append(intermediateWrapper, value);
+          intermediateWrapper = null;
+        }
+      });
+
+      // If we only had one element returned, and not an Node.ELEMENT_NODE
+      // we need to add it to $new_content
+      if (elementsReturned === 1) {
+        $new_content.append(intermediateWrapper);
       }
 
+      $new_content = $new_content.children();
+
       // If removing content from the wrapper, detach behaviors first.
       switch (method) {
         case 'html':
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
index f1c73064bd..772a05f734 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.libraries.yml
@@ -1,3 +1,8 @@
+ajax_insert:
+  js:
+    js/insert-ajax.js: {}
+  dependencies:
+    - core/drupal.ajax
 order:
  drupalSettings:
    ajax: test
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index e8d06c0a9f..89191a1c63 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -6,6 +6,14 @@ ajax_test.dialog_contents:
   requirements:
     _access: 'TRUE'
 
+ajax_test.ajax_render_types:
+  path: '/ajax-test/dialog-contents-types/{type}'
+  defaults:
+    _title: 'AJAX Dialog contents routing'
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderTypes'
+  requirements:
+    _access: 'TRUE'
+
 ajax_test.dialog_form:
   path: '/ajax-test/dialog-form'
   defaults:
@@ -21,6 +29,13 @@ ajax_test.dialog:
   requirements:
     _access: 'TRUE'
 
+ajax_test.insert_links:
+  path: '/ajax-test/insert'
+  defaults:
+    _controller: '\Drupal\ajax_test\Controller\AjaxTestController::insertLinks'
+  requirements:
+    _access: 'TRUE'
+
 ajax_test.dialog_close:
   path: '/ajax-test/dialog-close'
   defaults:
diff --git a/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
new file mode 100644
index 0000000000..b2b9277393
--- /dev/null
+++ b/core/modules/system/tests/modules/ajax_test/js/insert-ajax.js
@@ -0,0 +1,25 @@
+/**
+ * @file
+ * Provides method to test ajax requests.
+ */
+
+(function ($, window, Drupal, drupalSettings) {
+  'use strict';
+
+  Drupal.behaviors.insertTest = {
+    attach: function (context, settings) {
+      $('.ajax-insert').once('ajax-insert').on('click', function (event) {
+        event.preventDefault();
+        var ajaxSettings = {
+          url: event.currentTarget.getAttribute('href'),
+          wrapper: 'ajax-target',
+          base: false,
+          element: false,
+          method: 'html'
+        };
+        var myAjaxObject = Drupal.ajax(ajaxSettings);
+        myAjaxObject.execute();
+      });
+    }
+  };
+})(jQuery, window, Drupal, drupalSettings);
diff --git a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
index 63cc0abf16..6eb44410b6 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Controller/AjaxTestController.php
@@ -43,6 +43,79 @@ public static function dialogContents() {
   }
 
   /**
+   * Example content for testing whether response should be wrapped in div.
+   *
+   * @param string $type
+   *   Type of response.
+   *
+   * @return array
+   *   Renderable array of AJAX response contents.
+   */
+  public static function renderTypes($type) {
+    switch ($type) {
+      case 'pre-wrapped':
+        $markup = '<div class="' . $type . '">' . $type . '</div>';
+        break;
+
+      case 'pre-wrapped-leading-whitespace':
+        $markup = ' <div class="' . $type . '">' . $type . '</div>';
+        break;
+
+      case 'not-wrapped':
+        $markup = $type;
+        break;
+
+      case 'mixed':
+        $markup = ' foo <!-- COMMENT -->  foo bar<div class="a class"><p>some string</p></div> additional wrapped strings, <!-- ANOTHER COMMENT --> <p>final string</p>';
+        break;
+    }
+
+    $content = [
+      '#title' => '<em>AJAX Dialog & contents</em>',
+      'content' => [
+        '#type' => 'inline_template',
+        '#template' => !empty($markup) ? $markup : '',
+      ],
+    ];
+
+    return $content;
+  }
+
+  /**
+   * Returns a render array of links that directly Drupal.ajax().
+   */
+  public function insertLinks() {
+    $types = [
+      'pre-wrapped',
+      'pre-wrapped-leading-whitespace',
+      'not-wrapped',
+      'mixed',
+    ];
+
+    $build['links'] = [
+      'ajax_target' => [
+        '#markup' => '<div id="ajax-target">Target</div>',
+      ],
+      'links' => [
+        '#theme' => 'links',
+        '#attached' => ['library' => ['ajax_test/ajax_insert']],
+      ],
+    ];
+
+    foreach ($types as $type) {
+      $build['links']['links']['#links'][$type] = [
+        'title' => "Link $type",
+        'url' => Url::fromRoute('ajax_test.ajax_render_types', ['type' => $type]),
+        'attributes' => [
+          'class' => ['ajax-insert'],
+        ],
+      ];
+    }
+
+    return $build;
+  }
+
+  /**
    * Returns a render array that will be rendered by AjaxRenderer.
    *
    * Verifies that the response incorporates JavaScript settings generated
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
index e05940537c..386133cf34 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/AjaxTest.php
@@ -82,4 +82,35 @@ public function testDrupalSettingsCachingRegression() {
     $this->assertNotContains($fake_library, $libraries);
   }
 
+  /**
+   * Tests that various AJAX responses are correctly wrapped.
+   */
+  public function testWrap() {
+    $assert = $this->assertSession();
+    $this->drupalGet('ajax-test/insert');
+
+    // Test that no additional wrapper is added when inserting already wrapped
+    // response data.
+    $this->clickLink('Link pre-wrapped');
+    $assert->assertWaitOnAjaxRequest();
+    $assert->responseContains('<div class="pre-wrapped">pre-wrapped</div>');
+
+    // Test that no additional empty leading div is added when the return
+    // value had a leading space.
+    $this->clickLink('Link pre-wrapped-leading-whitespace');
+    $assert->assertWaitOnAjaxRequest();
+    $assert->responseContains('<div id="ajax-target"><div class="pre-wrapped-leading-whitespace">pre-wrapped-leading-whitespace</div></div>');
+
+    // Test that unwrapped response data (text node) is inserted wrapped.
+    $this->clickLink('Link not-wrapped');
+    $assert->assertWaitOnAjaxRequest();
+    $assert->responseContains('<div>not-wrapped</div>');
+
+    // Test that wrappend and unwrapped response data is inserted correctly.
+    $this->clickLink('Link mixed');
+    $assert->assertWaitOnAjaxRequest();
+    $this->createScreenshot('/tmp/drupal/na4.jpg');
+    $assert->responseContains('<div> foo <!-- COMMENT -->  foo bar</div><div class="a class"><p>some string</p></div><div> additional wrapped strings, <!-- ANOTHER COMMENT --> </div><p>final string</p>');
+  }
+
 }
