diff --git a/includes/common.inc b/includes/common.inc
index da8996a1b9..6bf9e05697 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -6934,7 +6934,7 @@ function drupal_common_theme() {
       'variables' => array(),
     ),
     'table' => array(
-      'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'empty' => ''),
+      'variables' => array('header' => NULL, 'footer' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => TRUE, 'empty' => ''),
     ),
     'tablesort_indicator' => array(
       'variables' => array('style' => NULL),
diff --git a/includes/theme.inc b/includes/theme.inc
index 9b606e9fb1..cea717765a 100644
--- a/includes/theme.inc
+++ b/includes/theme.inc
@@ -1916,24 +1916,24 @@ function theme_breadcrumb($variables) {
  *   - header: An array containing the table headers. Each element of the array
  *     can be either a localized string or an associative array with the
  *     following keys:
- *     - "data": The localized title of the table column.
- *     - "field": The database field represented in the table column (required
- *       if user is to be able to sort on this column).
- *     - "sort": A default sort order for this column ("asc" or "desc"). Only
- *       one column should be given a default sort order because table sorting
+ *     - data: The localized title of the table column.
+ *     - field: The database field represented in the table column (required if
+ *       user is to be able to sort on this column).
+ *     - sort: A default sort order for this column ("asc" or "desc"). Only one
+ *       column should be given a default sort order because table sorting
  *       only applies to one column at a time.
  *     - Any HTML attributes, such as "colspan", to apply to the column header
  *       cell.
  *   - rows: An array of table rows. Every row is an array of cells, or an
  *     associative array with the following keys:
- *     - "data": an array of cells
+ *     - data: an array of cells
  *     - Any HTML attributes, such as "class", to apply to the table row.
- *     - "no_striping": a boolean indicating that the row should receive no
+ *     - no_striping: a boolean indicating that the row should receive no
  *       'even / odd' styling. Defaults to FALSE.
  *     Each cell can be either a string or an associative array with the
  *     following keys:
- *     - "data": The string to display in the table cell.
- *     - "header": Indicates this cell is a header.
+ *     - data: The string to display in the table cell.
+ *     - header: Indicates this cell is a header.
  *     - Any HTML attributes, such as "colspan", to apply to the table cell.
  *     Here's an example for $rows:
  *     @code
@@ -1945,9 +1945,11 @@ function theme_breadcrumb($variables) {
  *       // Row with attributes on the row and some of its cells.
  *       array(
  *         'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky')
- *       )
+ *       ),
  *     );
  *     @endcode
+ *   - footer: An array of table rows which will be printed within a <tfoot>
+ *     tag, in the same format as the rows element (see above).
  *   - attributes: An array of HTML attributes to apply to the table tag.
  *   - caption: A localized string to use for the <caption> tag.
  *   - colgroups: An array of column groups. Each element of the array can be
@@ -2054,7 +2056,7 @@ function theme_table($variables) {
         $header_count++;
       }
     }
-    $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
+    $variables['rows'][] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message')));
   }
 
   // Format the table header:
@@ -2074,45 +2076,51 @@ function theme_table($variables) {
     $ts = array();
   }
 
-  // Format the table rows:
-  if (count($rows)) {
-    $output .= "<tbody>\n";
-    $flip = array('even' => 'odd', 'odd' => 'even');
-    $class = 'even';
-    foreach ($rows as $number => $row) {
-      // Check if we're dealing with a simple or complex row
-      if (isset($row['data'])) {
-        $cells = $row['data'];
-        $no_striping = isset($row['no_striping']) ? $row['no_striping'] : FALSE;
-
-        // Set the attributes array and exclude 'data' and 'no_striping'.
-        $attributes = $row;
-        unset($attributes['data']);
-        unset($attributes['no_striping']);
-      }
-      else {
+  // Rows and footer have the same structure.
+  $sections = array(
+    'rows' => 'tbody',
+    'footer' => 'tfoot',
+  );
+  foreach ($sections as $section => $tag) {
+    if (!empty($variables[$section])) {
+      $output .= "<" . $tag . ">\n";
+      $flip = array('even' => 'odd', 'odd' => 'even');
+      $class = 'even';
+      foreach ($variables[$section] as $number => $row) {
         $cells = $row;
         $attributes = array();
-        $no_striping = FALSE;
-      }
-      if (count($cells)) {
-        // Add odd/even class
+        $no_striping = $section === 'footer';
+
+        // Check if we're dealing with a simple or complex row.
+        if (isset($row['data'])) {
+          $cells = $row['data'];
+          $no_striping = isset($row['no_striping']) ? $row['no_striping'] : $no_striping;
+
+          // Set the attributes array and exclude 'data' and 'no_striping'.
+          $attributes = $row;
+          unset($attributes['data']);
+          unset($attributes['no_striping']);
+        }
+
+        // Add odd/even class.
         if (!$no_striping) {
           $class = $flip[$class];
           $attributes['class'][] = $class;
         }
 
-        // Build row
-        $output .= ' <tr' . drupal_attributes($attributes) . '>';
-        $i = 0;
-        foreach ($cells as $cell) {
-          $cell = tablesort_cell($cell, $header, $ts, $i++);
-          $output .= _theme_table_cell($cell);
+        // Build row.
+        if (!empty($cells)) {
+          $output .= ' <tr' . drupal_attributes($attributes) . '>';
+          $i = 0;
+          foreach ($cells as $cell) {
+            $cell = tablesort_cell($cell, $header, $ts, $i++);
+            $output .= _theme_table_cell($cell);
+          }
+          $output .= " </tr>\n";
         }
-        $output .= " </tr>\n";
       }
+      $output .= "</" . $tag . ">\n";
     }
-    $output .= "</tbody>\n";
   }
 
   $output .= "</table>\n";
diff --git a/modules/simpletest/tests/theme.test b/modules/simpletest/tests/theme.test
index 5f095bd558..f899aee4c2 100644
--- a/modules/simpletest/tests/theme.test
+++ b/modules/simpletest/tests/theme.test
@@ -179,6 +179,27 @@ class ThemeTableTestCase extends DrupalWebTestCase {
   }
 
   /**
+   * Test that the 'footer' option works correctly.
+   */
+  function testThemeTableFooter() {
+    $footer = array(
+      array(
+        'data' => array(1),
+      ),
+      array('Foo'),
+    );
+
+    $table = array(
+      'rows' => array(),
+      'footer' => $footer,
+    );
+
+    $this->content = theme('table', $table);
+    $this->content = preg_replace('@>\s+<@', '><', $this->content);
+    $this->assertRaw('<tfoot><tr><td>1</td></tr><tr><td>Foo</td></tr></tfoot>', 'Table footer found.');
+  }
+
+  /**
    * Tableheader.js provides 'sticky' table headers, and is included by default.
    */
   function testThemeTableStickyHeaders() {
