diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php
index e3100abc69..c2349d8a28 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -508,6 +508,44 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
 		return false;
 	}
 
+	/**
+	 * Indicates if the currently-matched node expects a closing
+	 * token, or if it will self-close on the next step.
+	 *
+	 * Most HTML elements expect a closer, such as a P element or
+	 * a DIV element. Others, like an IMG element are void and don't
+	 * have a closing tag. Special elements, such as SCRIPT and STYLE,
+	 * are treated just like void tags. Text nodes and self-closing
+	 * foreign content will also act just like a void tag, immediately
+	 * closing as soon as the processor advances to the next token.
+	 *
+	 * @since 6.6.0
+	 *
+	 * @todo When adding support for foreign content, ensure that
+	 *       this returns false for self-closing elements in the
+	 *       SVG and MathML namespace.
+	 *
+	 * @return bool Whether to expect a closer for the currently-matched node,
+	 *              or `null` if not matched on any token.
+	 */
+	public function expects_closer() {
+		$token_name = $this->get_token_name();
+		if ( ! isset( $token_name ) ) {
+			return null;
+		}
+
+		return ! (
+			// Comments, text nodes, and other atomic tokens.
+			'#' === $token_name[0] ||
+			// Doctype declarations.
+			'html' === $token_name ||
+			// Void elements.
+			self::is_void( $token_name ) ||
+			// Special atomic elements.
+			in_array( $token_name, array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true )
+		);
+	}
+
 	/**
 	 * Steps through the HTML document and stop at the next tag, if any.
 	 *
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php
index 13c2d7b58e..9b773197ef 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessor.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php
@@ -182,6 +182,103 @@ class Tests_HtmlApi_WpHtmlProcessor extends WP_UnitTestCase {
 		);
 	}
 
+	/**
+	 * Ensure reporting that normal non-void HTML elements expect a closer.
+	 *
+	 * @ticket 61257
+	 */
+	public function test_expects_closer_regular_tags() {
+		$processor = WP_HTML_Processor::create_fragment( '<div><p><b><em>' );
+
+		$tags = 0;
+		while ( $processor->next_tag() ) {
+			$this->assertTrue(
+				$processor->expects_closer(),
+				"Should have expected a closer for '{$processor->get_tag()}', but didn't."
+			);
+			++$tags;
+		}
+
+		$this->assertSame(
+			4,
+			$tags,
+			'Did not find all the expected tags.'
+		);
+	}
+
+	/**
+	 * Ensure reporting that non-tag HTML nodes expect a closer.
+	 *
+	 * @ticket 61257
+	 *
+	 * @dataProvider data_self_contained_node_tokens
+	 *
+	 * @param string $self_contained_token String starting with HTML token that doesn't expect a closer,
+	 *                                     e.g. an HTML comment, text node, void tag, or special element.
+	 */
+	public function test_expects_closer_expects_no_closer_for_self_contained_tokens( $self_contained_token ) {
+		$processor   = WP_HTML_Processor::create_fragment( $self_contained_token );
+		$found_token = $processor->next_token();
+
+		if ( WP_HTML_Processor::ERROR_UNSUPPORTED === $processor->get_last_error() ) {
+			$this->markTestSkipped( "HTML '{$self_contained_token}' is not supported." );
+		}
+
+		$this->assertTrue(
+			$found_token,
+			"Failed to find any tokens in '{$self_contained_token}': check test data provider."
+		);
+
+		$this->assertFalse(
+			$processor->expects_closer(),
+			"Incorrectly expected a closer for node of type '{$processor->get_token_type()}'."
+		);
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_self_contained_node_tokens() {
+		$self_contained_nodes = array(
+			'Normative comment'                => array( '<!-- comment -->' ),
+			'Comment with invalid closing'     => array( '<!-- comment --!>' ),
+			'CDATA Section lookalike'          => array( '<![CDATA[ comment ]]>' ),
+			'Processing Instruction lookalike' => array( '<?ok comment ?>' ),
+			'Funky comment'                    => array( '<//wp:post-meta key=isbn>' ),
+			'Text node'                        => array( 'Trombone' ),
+		);
+
+		foreach ( self::data_void_tags() as $tag_name => $_name ) {
+			$self_contained_nodes[ "Void elements ({$tag_name})" ] = array( "<{$tag_name}>" );
+		}
+
+		foreach ( self::data_special_tags() as $tag_name => $_name ) {
+			$self_contained_nodes[ "Special atomic elements ({$tag_name})" ] = array( "<{$tag_name}>content</{$tag_name}>" );
+		}
+
+		return $self_contained_nodes;
+	}
+
+	/**
+	 * Data provider.
+	 *
+	 * @return array[]
+	 */
+	public static function data_special_tags() {
+		return array(
+			'IFRAME'   => array( 'IFRAME' ),
+			'NOEMBED'  => array( 'NOEMBED' ),
+			'NOFRAMES' => array( 'NOFRAMES' ),
+			'SCRIPT'   => array( 'SCRIPT' ),
+			'STYLE'    => array( 'STYLE' ),
+			'TEXTAREA' => array( 'TEXTAREA' ),
+			'TITLE'    => array( 'TITLE' ),
+			'XMP'      => array( 'XMP' ),
+		);
+	}
+
 	/**
 	 * Ensure non-nesting tags do not nest when processing tokens.
 	 *