diff --git a/core/lib/Drupal/Core/Render/Element/Markup.php b/core/lib/Drupal/Core/Render/Element/Markup.php
deleted file mode 100644
index ffa87ed..0000000
--- a/core/lib/Drupal/Core/Render/Element/Markup.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\Render\Element\Markup.
- */
-
-namespace Drupal\Core\Render\Element;
-
-use Drupal\Component\Utility\Html as HtmlUtility;
-use Drupal\Component\Utility\SafeMarkup;
-use Drupal\Component\Utility\Xss;
-use Drupal\Core\Render\SafeString;
-
-/**
- * Provides a render element for HTML as a string, with sanitization.
- *
- * Properties:
- * - #markup: Specifies that the array provides HTML markup directly. Unless
- *   the markup is very simple, such as an explanation in a paragraph tag, it
- *   is normally preferable to use #theme or #type instead, so that the theme
- *   can customize the markup. Note that the value is passed through
- *   \Drupal\Component\Utility\Xss::filterAdmin(), which strips known XSS
- *   vectors while allowing a permissive list of HTML tags that are not XSS
- *   vectors. (I.e, <script> and <style> are not allowed.) See
- *   \Drupal\Component\Utility\Xss::$adminTags for the list of tags that will
- *   be allowed. If your markup needs any of the tags that are not in this
- *   whitelist, then you can implement a theme hook and template file and/or
- *   an asset library. Alternatively, you can use the render array keys
- *   #safe_strategy and #allowed_tags to alter how #markup is made safe.
- * - #safe_strategy: If #markup is supplied this can be used to change
- *   how the string is made safe for render. By default, all #markup is filtered
- *   using Xss::adminFilter(). However, if the string should be escaped using
- *   Html::escape() then this should be set to Markup::SAFE_STRATEGY_ESCAPE.
- * - #allowed_tags: If #markup is supplied this can be used to change which tags
- *   are using to filter the markup. The value should be an array of tags that
- *   Xss::filter() would accept. If #safe_strategy is set to
- *   Markup::SAFE_STRATEGY_ESCAPE this value is ignored.
- *
- * Usage example:
- * @code
- * $output['admin_filtered_string'] = array(
- *   '#type' => 'markup',
- *   '#markup' => '<em>This is filtered using the admin tag list</em>',
- * );
- * $output['filtered_string'] = array(
- *   '#type' => 'markup',
- *   '#markup' => '<em>This is filtered</em>',
- *   '#allowed_tags' => ['strong'],
- * );
- * $output['escaped_string'] = array(
- *   '#type' => 'markup',
- *   '#markup' => '<em>This is escaped</em>',
- *   '#safe_strategy' => Markup::SAFE_STRATEGY_ESCAPE,
- * );
- * @endcode
- *
- * @see theme_render
- *
- * @ingroup sanitization
- *
- * @RenderElement("markup")
- */
-class Markup extends RenderElement {
-
-  /**
-   * #safe_strategy indicating #markup should be filtered.
-   *
-   * @see ::ensureMarkupIsSafe()
-   * @see \Drupal\Component\Utility\Xss::filter()
-   */
-  const SAFE_STRATEGY_FILTER = 'xss';
-
-  /**
-   * #safe_strategy indicating #markup should be escaped.
-   *
-   * @see ::ensureMarkupIsSafe()
-   * @see \Drupal\Component\Utility\Html::escape()
-   */
-  const SAFE_STRATEGY_ESCAPE = 'escape';
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getInfo() {
-    return [
-      '#pre_render' => [
-        [static::class, 'ensureMarkupIsSafe'],
-      ],
-    ];
-  }
-
-  /**
-   * Escapes or filters #markup as required.
-   *
-   * Drupal uses Twig's auto-escape feature to improve security. This feature
-   * automatically escapes any HTML that is not known to be safe. Due to this
-   * the render system needs to ensure that all markup it generates is marked
-   * safe so that Twig does not do any additional escaping.
-   *
-   * By default all #markup is filtered to protect against XSS using the admin
-   * tag list. Render arrays can alter the list of tags allowed by the filter
-   * using the #allowed_tags property. This value should be an array of tags
-   * that Xss::filter() would accept. Render arrays can escape #markup instead
-   * of XSS filtering by setting the #safe_strategy property to
-   * Markup:SAFE_STRATEGY_ESCAPE. If the escaping strategy is used #allowed_tags
-   * is ignored.
-   *
-   * @param array $elements
-   *   A render array with #markup set.
-   *
-   * @return \Drupal\Component\Utility\SafeStringInterface|string
-   *   The escaped markup wrapped in a SafeString object. If
-   *   SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
-   *   escaped or filtered again.
-   *
-   * @see \Drupal\Component\Utility\Html::escape()
-   * @see \Drupal\Component\Utility\Xss::filter()
-   * @see \Drupal\Component\Utility\Xss::adminFilter()
-   * @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_FILTER
-   * @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_ESCAPE
-   */
-  public static function ensureMarkupIsSafe(array $elements) {
-    if (empty($elements['#markup'])) {
-      return $elements;
-    }
-
-    $strategy = isset($elements['#safe_strategy']) ? $elements['#safe_strategy'] : static::SAFE_STRATEGY_FILTER;
-    if (SafeMarkup::isSafe($elements['#markup'])) {
-      // Nothing to do as #markup is already marked as safe.
-      return $elements;
-    }
-    elseif ($strategy == static::SAFE_STRATEGY_ESCAPE) {
-      $markup = HtmlUtility::escape($elements['#markup']);
-    }
-    else {
-      // The default behaviour is to XSS filter using the admin tag list.
-      $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
-      $markup = Xss::filter($elements['#markup'], $tags);
-    }
-
-    $elements['#markup'] = SafeString::create($markup);
-
-    return $elements;
-  }
-
-}
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index f2be9ce..328a2fb 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\Render;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
@@ -14,6 +15,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Render\Element\Markup;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Theme\ThemeManagerInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -301,12 +303,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $pre_bubbling_elements = [];
     $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
 
-    // If #markup is set, ensure #type is set. This allows to specify just
-    // #markup on an element without setting #type.
-    if (isset($elements['#markup']) && !isset($elements['#type'])) {
-      $elements['#type'] = 'markup';
-    }
-
     // If the default values for this element have not been loaded yet, populate
     // them.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
@@ -377,6 +373,12 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $elements = $new_elements;
       $elements['#lazy_builder_built'] = TRUE;
     }
+
+    // All render elements support #markup and #text_plain.
+    if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) {
+      $elements = $this->ensureMarkupIsSafe($elements);
+    }
+
     // Make any final changes to the element before it is rendered. This means
     // that the $element or the children can be altered or corrected before the
     // element is rendered into the final text.
@@ -756,4 +758,48 @@ protected function xssFilterAdminIfUnsafe($string) {
     return SafeString::create($string);
   }
 
+  /**
+   * Escapes #plain_text or filters #markup as required.
+   *
+   * Drupal uses Twig's auto-escape feature to improve security. This feature
+   * automatically escapes any HTML that is not known to be safe. Due to this
+   * the render system needs to ensure that all markup it generates is marked
+   * safe so that Twig does not do any additional escaping.
+   *
+   * By default all #markup is filtered to protect against XSS using the admin
+   * tag list. Render arrays can alter the list of tags allowed by the filter
+   * using the #allowed_tags property. This value should be an array of tags
+   * that Xss::filter() would accept. Render arrays can escape text instead
+   * of XSS filtering by setting the #plain_text property instead of #markup. If
+   * #plain_text is used #allowed_tags is ignored.
+   *
+   * @param array $elements
+   *   A render array with #markup set.
+   *
+   * @return \Drupal\Component\Utility\SafeStringInterface|string
+   *   The escaped markup wrapped in a SafeString object. If
+   *   SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
+   *   escaped or filtered again.
+   *
+   * @see \Drupal\Component\Utility\Html::escape()
+   * @see \Drupal\Component\Utility\Xss::filter()
+   * @see \Drupal\Component\Utility\Xss::adminFilter()
+   */
+  protected function ensureMarkupIsSafe(array $elements) {
+    if (empty($elements['#markup']) && empty($elements['#plain_text'])) {
+      return $elements;
+    }
+
+    if (!empty($elements['#plain_text'])) {
+      $elements['#markup'] = SafeString::create(Html::escape($elements['#plain_text']));
+    }
+    elseif (!SafeMarkup::isSafe($elements['#markup'])) {
+      // The default behaviour is to XSS filter using the admin tag list.
+      $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
+      $elements['#markup'] = SafeString::create(Xss::filter($elements['#markup'], $tags));
+    }
+
+    return $elements;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index 8fd05c3..fd816c2 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -267,11 +267,37 @@
  * - #markup: Specifies that the array provides HTML markup directly. Unless
  *   the markup is very simple, such as an explanation in a paragraph tag, it
  *   is normally preferable to use #theme or #type instead, so that the theme
- *   can customize the markup. For additional options related to #markup see
- *   \Drupal\Core\Render\Element\Markup.
+ *   can customize the markup. Note that the value is passed through
+ *   \Drupal\Component\Utility\Xss::filterAdmin(), which strips known XSS
+ *   vectors while allowing a permissive list of HTML tags that are not XSS
+ *   vectors. (I.e, <script> and <style> are not allowed.) See
+ *   \Drupal\Component\Utility\Xss::$adminTags for the list of tags that will
+ *   be allowed. If your markup needs any of the tags that are not in this
+ *   whitelist, then you can implement a theme hook and template file and/or
+ *   an asset library. Aternatively, you can use the render array key
+ *   #allowed_tags to alter which tags are filtered.
+ * - #plain_text: Specifies that the array provides text that needs to be
+ *   escaped. This value takes precedence over #markup if present.
+ * - #allowed_tags: If #markup is supplied this can be used to change which tags
+ *   are using to filter the markup. The value should be an array of tags that
+ *   Xss::filter() would accept. If #plain_text is set this value is ignored.
+ *
+ *   Usage example:
+ *   @code
+ *   $output['admin_filtered_string'] = array(
+ *     '#markup' => '<em>This is filtered using the admin tag list</em>',
+ *   );
+ *   $output['filtered_string'] = array(
+ *     '#markup' => '<em>This is filtered</em>',
+ *     '#allowed_tags' => ['strong'],
+ *   );
+ *   $output['escaped_string'] = array(
+ *     '#plain_text' => '<em>This is escaped</em>',
+ *   );
+ *   @endcode
+ *
  *   @see core.libraries.yml
  *   @see hook_theme()
- *   @see \Drupal\Core\Render\Element\Markup
  *
  * JavaScript and CSS assets are specified in the render array using the
  * #attached property (see @ref sec_attached).
diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php
index adad131..9cb5e54 100644
--- a/core/modules/contact/src/MessageForm.php
+++ b/core/modules/contact/src/MessageForm.php
@@ -130,12 +130,12 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['name']['#type'] = 'item';
       $form['name']['#value'] = $user->getUsername();
       $form['name']['#required'] = FALSE;
-      $form['name']['#markup'] = SafeMarkup::checkPlain($user->getUsername());
+      $form['name']['#plain_text'] = $user->getUsername();
 
       $form['mail']['#type'] = 'item';
       $form['mail']['#value'] = $user->getEmail();
       $form['mail']['#required'] = FALSE;
-      $form['mail']['#markup'] = SafeMarkup::checkPlain($user->getEmail());
+      $form['mail']['#plain_text'] = $user->getEmail();
     }
 
     // The user contact form has a preset recipient.
diff --git a/core/modules/contact/src/Tests/ContactPersonalTest.php b/core/modules/contact/src/Tests/ContactPersonalTest.php
index 102452e..e522f09 100644
--- a/core/modules/contact/src/Tests/ContactPersonalTest.php
+++ b/core/modules/contact/src/Tests/ContactPersonalTest.php
@@ -63,8 +63,13 @@ protected function setUp() {
    * Tests that mails for contact messages are correctly sent.
    */
   function testSendPersonalContactMessage() {
+    // Ensure that the web user's email needs escaping.
+    $mail = $this->webUser->getUsername() . '&escaped@example.com';
+    $this->webUser->setEmail($mail)->save();
     $this->drupalLogin($this->webUser);
 
+    $this->drupalGet('user/' . $this->contactUser->id() . '/contact');
+    $this->assertEscaped($mail);
     $message = $this->submitPersonalContact($this->contactUser);
     $mails = $this->drupalGetMails();
     $this->assertEqual(1, count($mails));
@@ -93,7 +98,9 @@ function testSendPersonalContactMessage() {
       '@sender_email' => $this->webUser->getEmail(),
       '@recipient_name' => $this->contactUser->getUsername()
     );
-    $this->assertText(SafeMarkup::format('@sender_name (@sender_email) sent @recipient_name an email.', $placeholders));
+    $this->assertRaw(SafeMarkup::format('@sender_name (@sender_email) sent @recipient_name an email.', $placeholders));
+    // Ensure an unescaped version of the email does not exist anywhere.
+    $this->assertNoRaw($this->webUser->getEmail());
   }
 
   /**
diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php
index 81f9156..375ed37 100644
--- a/core/modules/dblog/src/Controller/DbLogController.php
+++ b/core/modules/dblog/src/Controller/DbLogController.php
@@ -184,13 +184,16 @@ public function overview() {
     foreach ($result as $dblog) {
       $message = $this->formatMessage($dblog);
       if ($message && isset($dblog->wid)) {
+        // Truncate the title text to 256 chars of message. Attribute will
+        // escape and unsafe HTML entities in the final text.
+        $title = Unicode::truncate(Html::decodeEntities(strip_tags($message)), 256, TRUE, TRUE);
         // Truncate link_text to 56 chars of message. The l() call will escape
         // any unsafe HTML entities in the final text.
-        $log_text = Unicode::truncate(Html::decodeEntities(strip_tags($message)), 56, TRUE, TRUE);
+        $log_text = Unicode::truncate($title, 56, TRUE, TRUE);
         $message = $this->l($log_text, new Url('dblog.event', array('event_id' => $dblog->wid), array(
           'attributes' => array(
             // Provide a title for the link for useful hover hints.
-            'title' => Unicode::truncate(strip_tags($message), 256, TRUE, TRUE),
+            'title' => $title,
           ),
         )));
       }
diff --git a/core/modules/dblog/src/Tests/DbLogTest.php b/core/modules/dblog/src/Tests/DbLogTest.php
index f96a320..2f5499f 100644
--- a/core/modules/dblog/src/Tests/DbLogTest.php
+++ b/core/modules/dblog/src/Tests/DbLogTest.php
@@ -733,11 +733,12 @@ public function testTemporaryUser() {
    */
   public function testOverviewLinks() {
     $this->drupalLogin($this->adminUser);
-    $this->generateLogEntries(1, ['message' => "&lt;script&gt;alert('foo');&lt;/script&gt;<strong>Lorem</strong> ipsum dolor sit amet, consectetur adipiscing elit."]);
+    $this->generateLogEntries(1, ['message' => "&lt;script&gt;alert('foo');&lt;/script&gt;<strong>Lorem</strong> ipsum dolor sit amet, consectetur adipiscing & elit."]);
     $this->drupalGet('admin/reports/dblog');
     $this->assertResponse(200);
     // Make sure HTML tags are filtered out.
-    $this->assertRaw('title="&amp;lt;script&amp;gt;alert(&#039;foo&#039;);&amp;lt;/script&amp;gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Entry #0">&lt;script&gt;alert(&#039;foo&#039;);&lt;/script&gt;Lorem ipsum dolor sit…</a>');
+    $this->assertRaw('title="alert(&#039;foo&#039;);Lorem ipsum dolor sit amet, consectetur adipiscing &amp; elit. Entry #0">&lt;script&gt;alert(&#039;foo&#039;);&lt;/script&gt;Lorem ipsum dolor sit…</a>');
+    $this->assertNoRaw("<script>alert('foo');</script>");
   }
 
 }
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 3ffa639..5577da0 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -9,7 +9,6 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element\Markup;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
@@ -723,8 +722,7 @@ function search_excerpt($keys, $text, $langcode = NULL) {
     // text. We also need to re-encode any HTML special characters that we
     // entity-decoded above.
     return [
-      '#markup' => Unicode::truncate($text, 256, TRUE, TRUE),
-      '#safe_strategy' => Markup::SAFE_STRATEGY_ESCAPE,
+      '#plain_text' => Unicode::truncate($text, 256, TRUE, TRUE),
     ];
   }
 
diff --git a/core/modules/views/src/Tests/ViewsHooksTest.php b/core/modules/views/src/Tests/ViewsHooksTest.php
index 1d6bf3c..714a3ec 100644
--- a/core/modules/views/src/Tests/ViewsHooksTest.php
+++ b/core/modules/views/src/Tests/ViewsHooksTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\views\Tests;
 
-use Drupal\Core\Render\Element\Markup;
 use Drupal\Core\Render\RenderContext;
 use Drupal\views\Views;
 
@@ -108,8 +107,7 @@ public function testHooks() {
   public function testViewsPreRenderViewsFormViewsForm() {
     $element = [
       'output' => [
-        '#markup' => '<!--will-be-escaped--><!--will-be-not-escaped-->',
-        '#safe_strategy' => Markup::SAFE_STRATEGY_ESCAPE,
+        '#plain_text' => '<!--will-be-escaped--><!--will-be-not-escaped-->',
       ],
       '#substitutions' => ['#value' => []],
     ];
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index c7d2ad4..030b34e 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -101,6 +101,19 @@ public function providerTestRenderBasic() {
     $data[] = [[
       '#markup' => 'foo',
     ], 'foo'];
+    // Basic #plain_text based renderable array.
+    $data[] = [[
+      '#plain_text' => 'foo',
+    ], 'foo'];
+    // Mixing #plain_text and #markup based renderable array.
+    $data[] = [[
+      '#plain_text' => '<em>foo</em>',
+      '#markup' => 'bar',
+    ], '&lt;em&gt;foo&lt;/em&gt;'];
+    // Safe strings in #plain_text are are still escaped.
+    $data[] = [[
+      '#plain_text' => SafeString::create('<em>foo</em>'),
+    ], '&lt;em&gt;foo&lt;/em&gt;'];
     // Renderable child element.
     $data[] = [[
       'child' => ['#markup' => 'bar'],
@@ -119,11 +132,11 @@ public function providerTestRenderBasic() {
     ], "This is <em>alert('XSS')</em> <strong>test</strong>"];
     // Html escaping test.
     $data[] = [[
-      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", '#safe_strategy' => Markup::SAFE_STRATEGY_ESCAPE],
+      'child' => ['#plain_text' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>"],
     ], "This is &lt;script&gt;&lt;em&gt;alert(&#039;XSS&#039;)&lt;/em&gt;&lt;/script&gt; &lt;strong&gt;test&lt;/strong&gt;"];
     // XSS filtering by default test.
     $data[] = [[
-      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", '#safe_strategy' => 'nonsense'],
+      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>"],
     ], "This is <em>alert('XSS')</em> <strong>test</strong>"];
     // Ensure non-XSS tags are not filtered out.
     $data[] = [[
