diff --git a/core/includes/form.inc b/core/includes/form.inc
index b26966f3bf27266f12a454dcf38c67595508419f..2659d06ef2b24f4f8f789c3e0dcb8405a729fd12 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -2347,6 +2347,36 @@ function form_type_checkboxes_value($element, $input = FALSE) {
   }
 }
 
+/**
+ * Determines the value of a table form element.
+ *
+ * @param array $element
+ *   The form element whose value is being populated.
+ * @param array|false $input
+ *   The incoming input to populate the form element. If this is FALSE,
+ *   the element's default value should be returned.
+ *
+ * @return array
+ *   The data that will appear in the $form_state['values'] collection
+ *   for this element. Return nothing to use the default.
+ */
+function form_type_table_value(array $element, $input = FALSE) {
+  // If #multiple is FALSE, the regular default value of radio buttons is used.
+  if (!empty($element['#tableselect']) && !empty($element['#multiple'])) {
+    // Contrary to #type 'checkboxes', the default value of checkboxes in a
+    // table is built from the array keys (instead of array values) of the
+    // #default_value property.
+    // @todo D8: Remove this inconsistency.
+    if ($input === FALSE) {
+      $element += array('#default_value' => array());
+      return drupal_map_assoc(array_keys(array_filter($element['#default_value'])));
+    }
+    else {
+      return is_array($input) ? drupal_map_assoc($input) : array();
+    }
+  }
+}
+
 /**
  * Form value callback: Determines the value for a #type radios form element.
  *
@@ -3566,6 +3596,128 @@ function form_process_tableselect($element) {
   return $element;
 }
 
+/**
+ * #process callback for #type 'table' to add tableselect support.
+ *
+ * @param array $element
+ *   An associative array containing the properties and children of the
+ *   table element.
+ * @param array $form_state
+ *   The current state of the form.
+ *
+ * @return array
+ *   The processed element.
+ *
+ * @see form_process_tableselect()
+ * @see theme_tableselect()
+ */
+function form_process_table($element, &$form_state) {
+  if ($element['#tableselect']) {
+    if ($element['#multiple']) {
+      $value = is_array($element['#value']) ? $element['#value'] : array();
+    }
+    // Advanced selection behaviour makes no sense for radios.
+    else {
+      $element['#js_select'] = FALSE;
+    }
+    // Add a "Select all" checkbox column to the header.
+    // @todo D8: Rename into #select_all?
+    if ($element['#js_select']) {
+      $element['#attached']['library'][] = array('system', 'drupal.tableselect');
+      array_unshift($element['#header'], array('class' => array('select-all')));
+    }
+    // Add an empty header column for radio buttons or when a "Select all"
+    // checkbox is not desired.
+    else {
+      array_unshift($element['#header'], '');
+    }
+
+    if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
+      $element['#default_value'] = array();
+    }
+    // Create a checkbox or radio for each row in a way that the value of the
+    // tableselect element behaves as if it had been of #type checkboxes or
+    // radios.
+    foreach (element_children($element) as $key) {
+      // Do not overwrite manually created children.
+      if (!isset($element[$key]['select'])) {
+        // Determine option label; either an assumed 'title' column, or the
+        // first available column containing a #title or #markup.
+        // @todo Consider to add an optional $element[$key]['#title_key']
+        //   defaulting to 'title'?
+        $title = '';
+        if (!empty($element[$key]['title']['#title'])) {
+          $title = $element[$key]['title']['#title'];
+        }
+        else {
+          foreach (element_children($element[$key]) as $column) {
+            if (isset($element[$key][$column]['#title'])) {
+              $title = $element[$key][$column]['#title'];
+              break;
+            }
+            if (isset($element[$key][$column]['#markup'])) {
+              $title = $element[$key][$column]['#markup'];
+              break;
+            }
+          }
+        }
+        if ($title !== '') {
+          $title = t('Update !title', array('!title' => $title));
+        }
+
+        // Prepend the select column to existing columns.
+        $element[$key] = array('select' => array()) + $element[$key];
+        $element[$key]['select'] += array(
+          '#type' => $element['#multiple'] ? 'checkbox' : 'radio',
+          '#title' => $title,
+          '#title_display' => 'invisible',
+          // @todo If rows happen to use numeric indexes instead of string keys,
+          //   this results in a first row with $key === 0, which is always FALSE.
+          '#return_value' => $key,
+          '#attributes' => $element['#attributes'],
+        );
+        $element_parents = array_merge($element['#parents'], array($key));
+        if ($element['#multiple']) {
+          $element[$key]['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
+          $element[$key]['select']['#parents'] = $element_parents;
+        }
+        else {
+          $element[$key]['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
+          $element[$key]['select']['#parents'] = $element['#parents'];
+          $element[$key]['select']['#id'] = drupal_html_id('edit-' . implode('-', $element_parents));
+        }
+      }
+    }
+  }
+
+  return $element;
+}
+
+/**
+ * #element_validate callback for #type 'table'.
+ *
+ * @param array $element
+ *   An associative array containing the properties and children of the
+ *   table element.
+ * @param array $form_state
+ *   The current state of the form.
+ */
+function form_validate_table($element, &$form_state) {
+  // Skip this validation if the button to submit the form does not require
+  // selected table row data.
+  if (empty($form_state['triggering_element']['#tableselect'])) {
+    return;
+  }
+  if ($element['#multiple']) {
+    if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
+      form_error($element, t('No items selected.'));
+    }
+  }
+  elseif (!isset($element['#value']) || $element['#value'] === '') {
+    form_error($element, t('No item selected.'));
+  }
+}
+
 /**
  * Processes a machine-readable name form element.
  *
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 8a35b6ed8b9c446f331ba1effa40dc6b06dfa46b..da8b32807c649ca495b4a555e35ad8ba202af84e 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1852,6 +1852,99 @@ function theme_breadcrumb($variables) {
   return $output;
 }
 
+/**
+ * #pre_render callback to transform children of an element into #rows suitable for theme_table().
+ *
+ * This function converts sub-elements of an element of #type 'table' to be
+ * suitable for theme_table():
+ * - The first level of sub-elements are table rows. Only the #attributes
+ *   property is taken into account.
+ * - The second level of sub-elements is converted into columns for the
+ *   corresponding first-level table row.
+ *
+ * Simple example usage:
+ * @code
+ * $form['table'] = array(
+ *   '#type' => 'table',
+ *   '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')),
+ *   // Optionally, to add tableDrag support:
+ *   '#tabledrag' => array(
+ *     array('order', 'sibling', 'thing-weight'),
+ *   ),
+ * );
+ * foreach ($things as $row => $thing) {
+ *   $form['table'][$row]['#weight'] = $thing['weight'];
+ *
+ *   $form['table'][$row]['title'] = array(
+ *     '#type' => 'textfield',
+ *     '#default_value' => $thing['title'],
+ *   );
+ *
+ *   // Optionally, to add tableDrag support:
+ *   $form['table'][$row]['#attributes']['class'][] = 'draggable';
+ *   $form['table'][$row]['weight'] = array(
+ *     '#type' => 'textfield',
+ *     '#title' => t('Weight for @title', array('@title' => $thing['title'])),
+ *     '#title_display' => 'invisible',
+ *     '#size' => 4,
+ *     '#default_value' => $thing['weight'],
+ *     '#attributes' => array('class' => array('thing-weight')),
+ *   );
+ *
+ *   // The amount of link columns should be identical to the 'colspan'
+ *   // attribute in #header above.
+ *   $form['table'][$row]['edit'] = array(
+ *     '#type' => 'link',
+ *     '#title' => t('Edit'),
+ *     '#href' => 'thing/' . $row . '/edit',
+ *   );
+ * }
+ * @endcode
+ *
+ * @param array $element
+ *   A structured array containing two sub-levels of elements. Properties used:
+ *   - #tabledrag: The value is a list of arrays that are passed to
+ *     drupal_add_tabledrag(). The HTML ID of the table is prepended to each set
+ *     of arguments.
+ *
+ * @see system_element_info()
+ * @see theme_table()
+ * @see drupal_process_attached()
+ * @see drupal_add_tabledrag()
+ */
+function drupal_pre_render_table(array $element) {
+  foreach (element_children($element) as $first) {
+    $row = array('data' => array());
+    // Apply attributes of first-level elements as table row attributes.
+    if (isset($element[$first]['#attributes'])) {
+      $row += $element[$first]['#attributes'];
+    }
+    // Turn second-level elements into table row columns.
+    // @todo Do not render a cell for children of #type 'value'.
+    // @see http://drupal.org/node/1248940
+    foreach (element_children($element[$first]) as $second) {
+      // Assign the element by reference, so any potential changes to the
+      // original element are taken over.
+      $row['data'][] = array('data' => &$element[$first][$second]);
+    }
+    $element['#rows'][] = $row;
+  }
+
+  // Take over $element['#id'] as HTML ID attribute, if not already set.
+  element_set_attributes($element, array('id'));
+
+  // If the custom #tabledrag is set and there is a HTML ID, inject the table's
+  // HTML ID as first callback argument and attach the behavior.
+  if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) {
+    foreach ($element['#tabledrag'] as &$args) {
+      array_unshift($args, $element['#attributes']['id']);
+    }
+    $element['#attached']['drupal_add_tabledrag'] = $element['#tabledrag'];
+  }
+
+  return $element;
+}
+
 /**
  * Returns HTML for a table.
  *
diff --git a/core/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc
index c01fb937eae7d55f4251d7b312ed3656cc45026e..a62f8e3c14eb0ff90050aee4077e0407635a6893 100644
--- a/core/modules/filter/filter.admin.inc
+++ b/core/modules/filter/filter.admin.inc
@@ -18,7 +18,17 @@ function filter_admin_overview($form) {
   $fallback_format = filter_fallback_format();
 
   $form['#tree'] = TRUE;
+  $form['formats'] = array(
+    '#type' => 'table',
+    '#header' => array(t('Name'), t('Roles'), t('Weight'), t('Operations')),
+    '#tabledrag' => array(
+      array('order', 'sibling', 'text-format-order-weight'),
+    ),
+  );
   foreach ($formats as $id => $format) {
+    $form['formats'][$id]['#attributes']['class'][] = 'draggable';
+    $form['formats'][$id]['#weight'] = $format->weight;
+
     $links = array();
     $links['configure'] = array(
       'title' => t('configure'),
@@ -40,16 +50,20 @@ function filter_admin_overview($form) {
         'href' => "admin/config/content/formats/$id/disable",
       );
     }
+
     $form['formats'][$id]['roles'] = array('#markup' => $roles_markup);
-    $form['formats'][$id]['operations'] = array(
-      '#type' => 'operations',
-      '#links' => $links,
-    );
+
     $form['formats'][$id]['weight'] = array(
       '#type' => 'weight',
       '#title' => t('Weight for @title', array('@title' => $format->name)),
       '#title_display' => 'invisible',
       '#default_value' => $format->weight,
+      '#attributes' => array('class' => array('text-format-order-weight')),
+    );
+
+    $form['formats'][$id]['operations'] = array(
+      '#type' => 'operations',
+      '#links' => $links,
     );
   }
   $form['actions'] = array('#type' => 'actions');
@@ -74,41 +88,6 @@ function filter_admin_overview_submit($form, &$form_state) {
   drupal_set_message(t('The text format ordering has been saved.'));
 }
 
-/**
- * Returns HTML for the text format administration overview form.
- *
- * @param $variables
- *   An associative array containing:
- *   - form: A render element representing the form.
- *
- * @ingroup themeable
- */
-function theme_filter_admin_overview($variables) {
-  $form = $variables['form'];
-
-  $rows = array();
-  foreach (element_children($form['formats']) as $id) {
-    $form['formats'][$id]['weight']['#attributes']['class'] = array('text-format-order-weight');
-    $row = array(
-      'data' => array(
-        drupal_render($form['formats'][$id]['name']),
-        drupal_render($form['formats'][$id]['roles']),
-        drupal_render($form['formats'][$id]['weight']),
-        drupal_render($form['formats'][$id]['operations']),
-      ),
-      'class' => array('draggable'),
-    );
-    $rows[] = $row;
-  }
-  $header = array(t('Name'), t('Roles'), t('Weight'), t('Operations'));
-  $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'text-format-order')));
-  $output .= drupal_render_children($form);
-
-  drupal_add_tabledrag('text-format-order', 'order', 'sibling', 'text-format-order-weight');
-
-  return $output;
-}
-
 /**
  * Page callback: Displays the text format add/edit form.
  *
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 1aff07049e7972618957a8734a54f3d8c989d90a..c4b596a4ab4b36373172460f5a1a7ad496f5929c 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -73,10 +73,6 @@ function filter_help($path, $arg) {
  */
 function filter_theme() {
   return array(
-    'filter_admin_overview' => array(
-      'render element' => 'form',
-      'file' => 'filter.admin.inc',
-    ),
     'filter_admin_format_filter_order' => array(
       'render element' => 'element',
       'file' => 'filter.admin.inc',
diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc
index 568149623562aa7f2ddbe50e66bbb5b4c7b2ce39..74f100cac37de44b470867c09de18360e8c0cb21 100644
--- a/core/modules/node/node.admin.inc
+++ b/core/modules/node/node.admin.inc
@@ -418,7 +418,6 @@ function node_admin_content($form, $form_state) {
  * Returns the admin form object to node_admin_content().
  *
  * @see node_admin_nodes_submit()
- * @see node_admin_nodes_validate()
  * @see node_filter_form()
  * @see node_filter_form_submit()
  * @see node_multiple_delete_confirm()
@@ -453,7 +452,7 @@ function node_admin_nodes() {
   $form['options']['submit'] = array(
     '#type' => 'submit',
     '#value' => t('Update'),
-    '#validate' => array('node_admin_nodes_validate'),
+    '#tableselect' => TRUE,
     '#submit' => array('node_admin_nodes_submit'),
   );
 
@@ -523,27 +522,39 @@ function node_admin_nodes() {
   // Prepare the list of nodes.
   $languages = language_list(LANGUAGE_ALL);
   $destination = drupal_get_destination();
-  $options = array();
+  $form['nodes'] = array(
+    '#type' => 'table',
+    '#header' => $header,
+    '#empty' => t('No content available.'),
+  );
   foreach ($nodes as $node) {
     $l_options = $node->langcode != LANGUAGE_NOT_SPECIFIED && isset($languages[$node->langcode]) ? array('language' => $languages[$node->langcode]) : array();
-    $options[$node->nid] = array(
-      'title' => array(
-        'data' => array(
-          '#type' => 'link',
-          '#title' => $node->label(),
-          '#href' => 'node/' . $node->nid,
-          '#options' => $l_options,
-          '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))),
-        ),
-      ),
-      'type' => check_plain(node_get_type_label($node)),
-      'author' => theme('username', array('account' => $node)),
-      'status' => $node->status ? t('published') : t('not published'),
-      'changed' => format_date($node->changed, 'short'),
+    $form['nodes'][$node->nid]['title'] = array(
+      '#type' => 'link',
+      '#title' => $node->label(),
+      '#href' => 'node/' . $node->nid,
+      '#options' => $l_options,
+      '#suffix' => ' ' . theme('mark', array('type' => node_mark($node->nid, $node->changed))),
+    );
+    $form['nodes'][$node->nid]['type'] = array(
+      '#markup' => check_plain(node_get_type_label($node)),
+    );
+    $form['nodes'][$node->nid]['author'] = array(
+      '#theme' => 'username',
+      '#account' => $node,
+    );
+    $form['nodes'][$node->nid]['status'] = array(
+      '#markup' => $node->status ? t('published') : t('not published'),
+    );
+    $form['nodes'][$node->nid]['changed'] = array(
+      '#markup' => format_date($node->changed, 'short'),
     );
     if ($multilingual) {
-      $options[$node->nid]['language_name'] = language_name($node->langcode);
+      $form['nodes'][$node->nid]['language_name'] = array(
+        '#markup' => language_name($node->langcode),
+      );
     }
+
     // Build a list of all the accessible operations for the current node.
     $operations = array();
     if (node_access('update', $node)) {
@@ -567,27 +578,23 @@ function node_admin_nodes() {
         'query' => $destination,
       );
     }
-    $options[$node->nid]['operations'] = array();
+    $form['nodes'][$node->nid]['operations'] = array();
     if (count($operations) > 1) {
       // Render an unordered list of operations links.
-      $options[$node->nid]['operations'] = array(
-        'data' => array(
-          '#type' => 'operations',
-          '#subtype' => 'node',
-          '#links' => $operations,
-        ),
+      $form['nodes'][$node->nid]['operations'] = array(
+        '#type' => 'operations',
+        '#subtype' => 'node',
+        '#links' => $operations,
       );
     }
     elseif (!empty($operations)) {
       // Render the first and only operation as a link.
       $link = reset($operations);
-      $options[$node->nid]['operations'] = array(
-        'data' => array(
-          '#type' => 'link',
-          '#title' => $link['title'],
-          '#href' => $link['href'],
-          '#options' => array('query' => $link['query']),
-        ),
+      $form['nodes'][$node->nid]['operations'] = array(
+        '#type' => 'link',
+        '#title' => $link['title'],
+        '#href' => $link['href'],
+        '#options' => array('query' => $link['query']),
       );
     }
   }
@@ -595,47 +602,13 @@ function node_admin_nodes() {
   // Only use a tableselect when the current user is able to perform any
   // operations.
   if ($admin_access) {
-    $form['nodes'] = array(
-      '#type' => 'tableselect',
-      '#header' => $header,
-      '#options' => $options,
-      '#empty' => t('No content available.'),
-    );
-  }
-  // Otherwise, use a simple table.
-  else {
-    $form['nodes'] = array(
-      '#theme' => 'table',
-      '#header' => $header,
-      '#rows' => $options,
-      '#empty' => t('No content available.'),
-    );
+    $form['nodes']['#tableselect'] = TRUE;
   }
 
-  $form['pager'] = array('#markup' => theme('pager'));
+  $form['pager'] = array('#theme' => 'pager');
   return $form;
 }
 
-/**
- * Form validation handler for node_admin_nodes().
- *
- * Checks whether any nodes have been selected to perform the chosen 'Update
- * option' on.
- *
- * @see node_admin_nodes()
- * @see node_admin_nodes_submit()
- * @see node_filter_form()
- * @see node_filter_form_submit()
- * @see node_multiple_delete_confirm()
- * @see node_multiple_delete_confirm_submit()
- */
-function node_admin_nodes_validate($form, &$form_state) {
-  // Error if there are no items to select.
-  if (!is_array($form_state['values']['nodes']) || !count(array_filter($form_state['values']['nodes']))) {
-    form_set_error('', t('No items selected.'));
-  }
-}
-
 /**
  * Form submission handler for node_admin_nodes().
  *
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index eaaebc2c583c30dbf971be770f6a962f6a12931b..709fd1f52b1bba4f0a4c0e561fb690c6d6457f9e 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -557,6 +557,30 @@ function system_element_info() {
     '#theme' => 'hidden',
   );
 
+  $types['table'] = array(
+    '#header' => array(),
+    '#rows' => array(),
+    '#empty' => '',
+    // Properties for tableselect support.
+    '#input' => TRUE,
+    '#tree' => TRUE,
+    '#tableselect' => FALSE,
+    '#multiple' => TRUE,
+    '#js_select' => TRUE,
+    '#value_callback' => 'form_type_table_value',
+    '#process' => array('form_process_table'),
+    '#element_validate' => array('form_validate_table'),
+    // Properties for tabledrag support.
+    // The value is a list of arrays that are passed to drupal_add_tabledrag().
+    // drupal_pre_render_table() prepends the HTML ID of the table to each set
+    // of arguments.
+    // @see drupal_add_tabledrag()
+    '#tabledrag' => array(),
+    // Render properties.
+    '#pre_render' => array('drupal_pre_render_table'),
+    '#theme' => 'table',
+  );
+
   return $types;
 }